/*
 * 
 *  This file is part of Sourceprojects.org Lycia Copyright 2010 noctarius
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * 
 */
package org.sourceprojects.lycia.internal.configuration;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.sourceprojects.lycia.TypeConverter;
import org.sourceprojects.lycia.EndTagAware;
import org.sourceprojects.lycia.GeneratorFactory;
import org.sourceprojects.lycia.LyciaConfigurator;
import org.sourceprojects.lycia.LyciaParser;
import org.sourceprojects.lycia.QualifiedName;
import org.sourceprojects.lycia.TagDefinitionParser;
import org.sourceprojects.lycia.GenerationStrategy;
import org.sourceprojects.lycia.annotations.Tag;
import org.sourceprojects.lycia.exceptions.LyciaConfigurationException;
import org.sourceprojects.lycia.exceptions.LyciaInitializationException;
import org.sourceprojects.lycia.internal.Cache;
import org.sourceprojects.lycia.internal.InternalParserContext;
import org.sourceprojects.lycia.internal.TagDefinitionParserContainer;
import org.sourceprojects.lycia.internal.dataconverter.BaseDataConverter;
import org.sourceprojects.lycia.internal.dataconverter.ByteDataConverter;
import org.sourceprojects.lycia.internal.generator.GeneratorFactoryImpl;
import org.sourceprojects.lycia.internal.logging.LyciaLoggerWrapper;
import org.sourceprojects.lycia.internal.platform.PlatformFactory;
import org.sourceprojects.lycia.internal.platform.PlatformFactoryProvider;
import org.w3c.dom.Element;
import org.xml.sax.ContentHandler;

/**
 * Base implementation of {@link LyciaConfigurator}.
 * 
 * @author noctarius
 * @since 1.9.1
 * 
 * @param <B>
 *            Type of ContextObject
 */
public class BasicXmlParserConfigurator<B> implements LyciaConfigurator<B> {
	private static final LyciaLoggerWrapper LOGGER = LyciaLoggerWrapper
			.getLogger(BasicXmlParserConfigurator.class);

	private static final Pattern DEFAULT_PATTERN = Pattern.compile("\\*");

	private final TagDefinitionParserContainer<B> tagDefinitionParsers = new TagDefinitionParserContainer<B>();
	private final List<EndTagAware<B>> endTagReachedListeners = new ArrayList<EndTagAware<B>>();
	private final List<ContentHandler> contentHandlers = new ArrayList<ContentHandler>();
	private final List<TypeConverter> typeConverters = new ArrayList<TypeConverter>();
	private final PlatformFactory<B> platformFactory;
	private final LyciaConfigurator<B> parent;
	private final LyciaParser<B> xmlParser;

	private GenerationStrategy generationStrategy = GenerationStrategy.ReflectiveGeneration;
	private GeneratorFactory<B> generatorFactory = new GeneratorFactoryImpl<B>();
	private boolean nullAware = false;
	private boolean ignoreComments = true;

	public BasicXmlParserConfigurator(final LyciaParser<B> xmlParser,
			final LyciaConfigurator<B> parent) {

		this.platformFactory = PlatformFactoryProvider.newInstance(xmlParser);
		this.xmlParser = xmlParser;
		this.parent = parent;

		// Register base data converters needed for basic conversion of the
		// attributed method parameters
		typeConverters.add(BaseDataConverter.BASE_DATA_CONVERTER);
		typeConverters.add(ByteDataConverter.BASE_BYTE_DATA_CONVERTER);
	}

	@Override
	public void addTagDefinitionParser(
			final TagDefinitionParser<B> definitionParser) {
		addTagDefinitionParserInternal(definitionParser);
	}

	@Override
	public void addTagDefinitionParsers(
			final TagDefinitionParser<B>[] definitionParsers) {
		for (final TagDefinitionParser<B> definitionParser : definitionParsers) {
			addTagDefinitionParserInternal(definitionParser);
		}
	}

	@Override
	public void addTagDefinitionParser(final Object definitionParser) {
		addTagDefinitionParserInternal(definitionParser);
	}

	@Override
	public void addTagDefinitionParsers(final Object[] definitionParsers) {
		for (final Object definitionParser : definitionParsers) {
			addTagDefinitionParserInternal(definitionParser);
		}
	}

	@Override
	public TagDefinitionParser<B> findTagDefinitionParser(
			final QualifiedName localElement, final Element element) {
		final List<TagDefinitionParser<B>> parsers = findTagDefinitionParsers(
				localElement, element);

		if (parsers.size() == 0) {
			return null;
		}

		return parsers.get(0);
	}

	@Override
	public TagDefinitionParser<B> findTagDefinitionParser(final String tagName,
			final String namespaceURI, final Element element) {
		return findTagDefinitionParser(
				new QualifiedName(namespaceURI, tagName), element);
	}

	@Override
	public List<TagDefinitionParser<B>> findTagDefinitionParsers(
			final QualifiedName localElement, final Element element) {
		return findTagDefinitionParserInternal(localElement.getNamespaceURI(),
				localElement.getLocalPart(), element);
	}

	@Override
	public List<TagDefinitionParser<B>> findTagDefinitionParsers(
			final String tagName, final String namespaceURI,
			final Element element) {
		return findTagDefinitionParsers(
				new QualifiedName(namespaceURI, tagName), element);
	}

	@Override
	public void removeTagDefinitionParser(
			final TagDefinitionParser<B> definitionParser) {
		removeTagDefinitionParserInternal(definitionParser);
	}

	@Override
	public void removeTagDefinitionParsers(
			final TagDefinitionParser<B>[] definitionParsers) {
		for (final TagDefinitionParser<B> definitionParser : definitionParsers) {
			removeTagDefinitionParserInternal(definitionParser);
		}
	}

	@Override
	public void removeTagDefinitionParser(final Object definitionParser) {
		removeTagDefinitionParserInternal(definitionParser);
	}

	@Override
	public void removeTagDefinitionParsers(final Object[] definitionParsers) {
		for (final Object definitionParser : definitionParsers) {
			removeTagDefinitionParserInternal(definitionParser);
		}
	}

	@Override
	public GenerationStrategy getGenerationStrategy() {
		return generationStrategy;
	}

	@Override
	public void setGenerationStrategy(
			final GenerationStrategy generationStrategy) {
		this.generationStrategy = generationStrategy;
	}

	public LyciaParser<B> getXmlParser() {
		return xmlParser;
	}

	@Override
	public LyciaConfigurator<B> getParentXmlParserConfigurator() {
		return parent;
	}

	@Override
	public GeneratorFactory<B> getGeneratorFactory() {
		return generatorFactory;
	}

	@Override
	public void setGeneratorFactory(final GeneratorFactory<B> generatorFactory) {
		this.generatorFactory = generatorFactory;
	}

	@Override
	public void addContentHandler(final ContentHandler contentHandler) {
		contentHandlers.add(contentHandler);
	}

	@Override
	public void removeContentHandler(final ContentHandler contentHandler) {
		contentHandlers.remove(contentHandler);
	}

	@Override
	public List<ContentHandler> getContentHandlers() {
		return contentHandlers;
	}

	protected void addTagDefinitionParserToContainer(
			final TagDefinitionParser<B> tagDefinitionParser,
			final String namespaceURI) {

		for (final Tag tag : tagDefinitionParser.getTagAnnotations()) {
			if (isAlreadyRegistered(tag, namespaceURI)) {
				throw new LyciaInitializationException(
						namespaceURI
								+ "=>"
								+ tag.value()
								+ " has already a registered parser. Duplicate parsers "
								+ "for same tag-expression is illegal");
			}
		}

		Set<TagDefinitionParser<B>> parsers = tagDefinitionParsers
				.get(namespaceURI);
		if (parsers == null) {
			parsers = new HashSet<TagDefinitionParser<B>>();
		}

		if (LOGGER.isDebugEnabled()) {
			for (final Tag tag : tagDefinitionParser.getTagAnnotations()) {
				LOGGER.debug("Registered TagDefinitionParser '"
						+ tagDefinitionParser + "' for tag '" + tag.namespace()
						+ "->" + tag.value());
			}
		}

		parsers.add(tagDefinitionParser);
		tagDefinitionParsers.put(namespaceURI, parsers);
	}

	protected QualifiedName[] generateParentQNames(final String tag,
			final String namespaceURI) {

		return TagConfigurationUtils.generateParentQNames(tag, namespaceURI);
	}

	protected InternalParserContext<B> getInternalParserContextRepresentation() {
		if (!(xmlParser.getParserContext() instanceof InternalParserContext<?>)) {
			return null;
		}

		return (InternalParserContext<B>) xmlParser.getParserContext();
	}

	private boolean isAlreadyRegistered(final Tag tag, final String namespaceURI) {
		final Set<TagDefinitionParser<B>> parsers = tagDefinitionParsers
				.get(namespaceURI);

		if (parsers == null) {
			return false;
		}

		for (final TagDefinitionParser<B> parser : parsers) {
			for (final Tag parserTag : parser.getTagAnnotations()) {
				if (tag.value().equals(parserTag.value())
						&& tag.namespace().equals(parserTag.namespace())) {
					return true;
				}
			}
		}

		return false;
	}

	private List<TagDefinitionParser<B>> findTagDefinitionParserInternal(
			final String namespaceURI, final String tagname,
			final Element element) {
		final List<TagDefinitionParser<B>> result = new ArrayList<TagDefinitionParser<B>>();

		TagDefinitionParser<B> defaultParser = null;

		final Set<TagDefinitionParser<B>> parsers = tagDefinitionParsers
				.get(namespaceURI);

		if (parsers == null) {
			throw new LyciaConfigurationException(
					"No namespace parsers found for namespace " + namespaceURI);
		}

		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Try to find TagDefinitionParser for " + namespaceURI
					+ "::" + tagname);
		}

		for (final TagDefinitionParser<B> parser : parsers) {
			for (final Tag tag : parser.getTagAnnotations()) {
				final String expression = tag.value().trim();
				final String namespace = "".equals(tag.namespace()) ? namespaceURI
						: tag.namespace();

				final Pattern pattern = buildPattern(tag);
				if (pattern == null && expression.contains("\\")) {
					final QualifiedName[] qNames = generateParentQNames(
							expression, namespace);

					if (TagConfigurationUtils.matchesElement(element, qNames)) {
						result.add(parser);
					}
				} else if (pattern != null) {
					final Matcher matcher = pattern.matcher(tagname);
					if (matcher.matches()) {
						result.add(parser);
					}
				} else if (expression.equals(tagname)
						&& namespace.equals(namespaceURI)) {
					result.add(parser);
				} else if (expression.equals("*")
						&& namespace.equals(namespaceURI)) {
					if (defaultParser != null) {
						throw new LyciaConfigurationException(
								"Only one default TagDefinitionParser can be registered for expression "
										+ expression);
					}

					defaultParser = parser;
					result.add(parser);
				}
			}
		}

		return result;
	}

	@SuppressWarnings("unchecked")
	private void addTagDefinitionParserInternal(final Object tagDefinitionParser) {
		if (xmlParser.getResourceResolver() == null) {
			throw new NullPointerException("ResourceResolver is null");
		}

		// Assume we have a old fashioned TagDefinitionParser
		if (tagDefinitionParser instanceof TagDefinitionParser<?>) {
			addOldFashionedTagDefinitionParser((TagDefinitionParser<B>) tagDefinitionParser);
			return;
		}

		// Try to handle tagDefinitionParser by parent
		if (parent != null) {
			parent.addTagDefinitionParser(tagDefinitionParser);
		}
	}

	private void addOldFashionedTagDefinitionParser(
			final TagDefinitionParser<B> tagDefinitionParser) {
		final String namespaceURI = tagDefinitionParser.getNamespace();

		if (xmlParser.getResourceResolver().resolveResource(null, namespaceURI,
				null, null, null) == null) {
			throw new LyciaConfigurationException(new NullPointerException(
					"Namespace " + namespaceURI + " cannot be resolved"));
		}

		addTagDefinitionParserToContainer(tagDefinitionParser, namespaceURI);
	}

	private Pattern buildPattern(final Tag tag) {
		if (!tag.regex()) {
			return null;
		}

		final InternalParserContext<B> context = getInternalParserContextRepresentation();
		Cache<Pattern> patternCache = null;
		if (context != null) {
			patternCache = context.getContextUtils().getPatternCache();
		}

		Pattern pattern = null;

		if (patternCache != null) {
			pattern = patternCache.get(String.valueOf(tag.hashCode()));
		}

		if (pattern == null) {
			try {
				pattern = Pattern.compile(tag.value());
			} catch (final PatternSyntaxException e) {
				pattern = DEFAULT_PATTERN;
			}

			if (patternCache != null) {
				patternCache.put(String.valueOf(tag.hashCode()), pattern,
						xmlParser.getCacheExpirationTime());
			}
		}

		return pattern == DEFAULT_PATTERN ? null : pattern;
	}

	@SuppressWarnings("unchecked")
	private void removeTagDefinitionParserInternal(
			final Object tagDefinitionParser) {
		String namespaceURI = null;

		if (tagDefinitionParser instanceof TagDefinitionParser<?>) {
			namespaceURI = ((TagDefinitionParser<B>) tagDefinitionParser)
					.getNamespace();
		}

		final Set<TagDefinitionParser<B>> parsers = tagDefinitionParsers
				.get(namespaceURI);

		if (parsers != null) {
			parsers.remove(tagDefinitionParser);
			tagDefinitionParsers.put(namespaceURI, parsers);
		}
	}

	@Override
	public PlatformFactory<B> getPlatformFactory() {
		return platformFactory;
	}

	@Override
	public boolean isNullAware() {
		return nullAware;
	}

	@Override
	public void setNullAware(boolean nullAware) {
		this.nullAware = nullAware;
	}

	@Override
	public void addEndTagReachedListener(EndTagAware<B> listener) {
		endTagReachedListeners.add(listener);
	}

	@Override
	public void removeEndTagReachedListener(EndTagAware<B> listener) {
		endTagReachedListeners.remove(listener);
	}

	@Override
	public List<EndTagAware<B>> getEndTagReachedListeners() {
		return Collections.unmodifiableList(endTagReachedListeners);
	}

	@Override
	public boolean isIgnoringComments() {
		return ignoreComments;
	}

	@Override
	public void setIgnoringComments(boolean ignore) {
		ignoreComments = ignore;
	}

	@Override
	public void addGlobalDataConverter(TypeConverter typeConverter) {
		typeConverters.add(typeConverter);
	}

	@Override
	public void removeGlobalDataConverter(TypeConverter typeConverter) {
		typeConverters.remove(typeConverter);
	}

	@Override
	public List<TypeConverter> getTypeConverters() {
		return Collections.unmodifiableList(typeConverters);
	}

}
